package edu.mercer.sse658.amazonbkp;

import java.io.FileWriter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.logging.StreamHandler;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;

import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.collect.Range;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkArgument;

/**
 * Custom Logger Handler.<p>
 * 
 * Buffers, writes log to disk. Accepts filename / filename and path String.
 *
 * @see CustomLogFormatter
 */

final class LogCustom extends StreamHandler
{
	/** 
	 * ConcurrentMap Collection of Logger instances to provide multi-thread
	 * access to Logger instances.<p>
	 *  
	 * <ul><li>Key - String log name as supplied to constructor
	 *     <li>Value - Custom Logger
	 */
	
	private transient final static ConcurrentMap<String,CustomLogger> _logs = 
			new ConcurrentHashMap<String,CustomLogger>();

	/** Optional containing Path to log file OR null for this instance */
	
	private transient Optional<Path> _logFilePath = Optional.absent();

	/** Optional containing FileWriter prepared by constructor */
	
	private transient Optional<FileWriter> _fileWriter = Optional.absent();
	
	/** Optional containing Custom log formatter */
	
	private transient Optional<CustomLogFormatter> _formatter = Optional
			.absent();
	
	
	/**
	 * Constructs default StreamHandler for Logging: 'logfile.log'
	 * 
	 * @throws SecurityException  log file name output not permitted
	 * 
	 * @throws IOException  log file name file system error
	 * 
	 * @throws IllegalArgumentException  log file name invalid
	 * 
	 * @throws NullPointerException  log file name null
	 * 
	 * @throws InvalidPathException  log file name failed conversion to Path
	 * 
	 * @see LogCustom(String logFileName)
	 */
	
	private LogCustom() throws InvalidPathException, SecurityException, 
		IllegalArgumentException, NullPointerException, IOException
	{
		new LogCustom("logfile");
		
	}//constructor
	
	
	/**
	 * Constructs custom log file.<p>
	 * 
	 * Validates log file name, path in preparation for first I/O.
	 * 
	 * @param logFileName  String log file name for creation/output, not null
	 * 
	 * @throws SecurityException  log file name output not permitted
	 * 
	 * @throws IOException  log file name file system error
	 * 
	 * @throws IllegalArgumentException  log file name invalid; i.e., log path 
	 * 		unwritable, is Directory, ...
	 * 
	 * @throws NullPointerException  log file name null
	 * 
	 * @throws InvalidPathException  log file name failed conversion to Path
	 * 
	 * @see validLogFilePath
	 */
	
	private LogCustom(final String logFileName) throws 
		SecurityException, IOException, IllegalArgumentException, 
		NullPointerException, InvalidPathException
	{
		super();
		
		final Path logFilePathReq = validLogFilePath(logFileName);
		
		//Files.deleteIfExists(logFilePathReq);
		
		setLogFilePath(logFilePathReq);
		
		_formatter = Optional.of(new CustomLogFormatter());
		
		this.setFormatter(_formatter.get());
				
	}//constructor

	
	/**
	 * Ensures log file name specified is valid/writable.<p>
	 * 
	 * Adds suffix '.log' extension if omitted
	 * 
	 * @param logFileNameIn  String log file name for creation/output, not null
	 * 
	 * @return Path  log file name String converted into Path
	 * 
	 * @throws SecurityException  log file name output not permitted
	 * 
	 * @throws IOException  log file name file system error
	 * 
	 * @throws IllegalArgumentException  log file name invalid
	 * 
	 * @throws NullPointerException  log file name null
	 * 
	 * @throws InvalidPathException  log file name failed conversion to Path
	 */
	
	private Path validLogFilePath(final String logFileNameIn) throws 
		SecurityException, IOException, IllegalArgumentException, 
		NullPointerException, InvalidPathException
	{
		checkNotNull(logFileNameIn, "Null log file name");
		
		final Range<Integer> fileNameLength = Range.closed(3, 80);
		
		checkArgument(fileNameLength.contains(logFileNameIn.length()), 
				"Log filename outside expected range");

		final String logFileName = logFileNameIn.matches("\\.log$") ? 
				logFileNameIn : logFileNameIn + ".log";
		
		final Matcher validFileName = Pattern.compile(
				
				"[a-zA-Z\\._\\-\\+\\\\0-9\\/]{"+
				
				fileNameLength.lowerEndpoint()
				+","+
				fileNameLength.upperEndpoint()
				+"}")
				.matcher(logFileName);
		
		checkArgument(validFileName.matches(), "Log Filename invalid " +
				logFileName);
		
		Path logFilePath = Paths.get(logFileName);

		checkArgument( !Files.isDirectory(logFilePath), "Log filename is " +
				"directory : " + logFilePath);
		
		checkArgument( !Files.isHidden(logFilePath), "Log filename is hidden "+
				logFilePath);
		
		if( Files.exists(logFilePath) )
		{ 
			checkArgument( Files.isWritable(logFilePath), "Log filename not " +
				"writable " + logFilePath);

		}//if
		
		return logFilePath;
		
	}//method
	
	
	/**
	 * Factory: creates or retrieves custom logger instance.
	 * 
	 * @param logName  String suggest Class name, not null
	 * 
	 * @param logLvl  Level, not null (Example: LogLevel.INFO)
	 * 
	 * @return Logger  Instance of custom logger
	 * 
	 * @throws LoggerException  Unable to open log file
	 */
	
	public synchronized static CustomLogger obtainLog(final String logName, 
			final Level logLvl) 
	{
		try
		{
		
			checkNotNull(logName, "Null log name");

			checkNotNull(logLvl, "Null log level");

			if( _logs.containsKey(logName) )
			{ 
				return _logs.get(logName);

			}//if

			final CustomLogger logger = CustomLogger.getLogger(logName);

			final LogCustom custLogHandler = new LogCustom(logName);

			logger.addHandler(custLogHandler);

			logger.setUseParentHandlers(false);

			logger.setLevel(logLvl);

			_logs.put(logName, logger);

			checkNotNull( _logs.get(logName), 
						  "Unable retrieve Logger from Map");
		
		}
		catch( final IOException err )
		{
			throw new LoggerException( err );
			
		}//try
		
		return _logs.get( logName );
		 
	}//method
	
	
	/**
	 * Factory: creates or retrieves custom logger instance.<p>
	 * 
	 * Default Logger level of SEVERE
	 * 
	 * @param logName  String suggest Class name, not null
	 * 
	 * @return Logger  Custom logger instance
	 * 
	 * @throws LoggerException  Unable to open log file
	 * 
	 * @see obtainLog(String logName, Level logLvl)
	 */
	
	public static CustomLogger obtainLog(final String logName) 
	{
		return obtainLog(logName, Level.SEVERE);
		
	}//method
	

	/**
	 * Formats and outputs log record to file writer.<p>
	 * 
	 * Synchronized protection for FileWriter
	 * 
	 * @param recIn  LogRecord, not null
	 * 
	 * @throws NullPointerException  recIn is null OR FileWriter uninitialized
	 * 
	 * @throws UnsupportedOperationException  FileWriter Path.toFile 
	 * 		conversion exception
	 * 
	 * @see StreamHandler
	 */
	
	@Override
	public synchronized void publish(final LogRecord recIn) 
	{
		checkNotNull(recIn, "Null detected LogRecord upon publsh call");
		
		if(recIn.getMessage().length() == 0) return;
		
		if( !isLoggable(recIn) ) return;
		
		final String logMsg = _formatter.get().format(recIn);
		
		try
		{
			if( !_fileWriter.isPresent() ) 
			{
				_fileWriter = Optional.of(new FileWriter(getLogFilePath().get()
					.toFile()));
			
			}//if
			
			_fileWriter.get().append(logMsg);
			
		}
		catch(IOException err)
		{
			Logger.getGlobal().severe("Unable to publish: " + logMsg + " " + 
					err);

		}
		catch(final Throwable unexpErr)
		{
			Throwables.propagateIfPossible(unexpErr);
			throw new RuntimeException(unexpErr);
			
		}//try
		
	}//method


	/**
	 * Flushes output to file writer.<p>
	 * 
	 * Silently does nothing if publish not yet called as FileWriter 
	 * uninitialized.
	 */
	
	@Override
	public synchronized void flush() 
	{
		try
		{		
			if( _fileWriter.isPresent() ) _fileWriter.get().flush();

		}
		catch(IOException err)
		{
			Logger.getGlobal().severe("Unable to flush log file writer " + 
					err);

		}
		catch(final Throwable unexpErr)
		{
			Throwables.propagateIfPossible(unexpErr);
			throw new RuntimeException(unexpErr);
			
		}//try
			
	}//method


	/**
	 * Closes file writer output.
	 */
	
	@Override
	public synchronized void close()
	{
		try
		{		
			if( _fileWriter.isPresent() ) _fileWriter.get().close();
		
		}
		catch(final IOException err)
		{			
			Logger.getGlobal().severe("Unable to close log file writer " + 
					err);
		
		}
		catch(final Throwable unexpErr)
		{
			Throwables.propagateIfPossible(unexpErr);
			throw new RuntimeException(unexpErr);

		}//try
		
	}//method

	
	/**
	 * Checks log message and supporting output.
	 * 
	 * @param recIn  LogRecord, not null
	 * 
	 * @return boolean  T: 
	 * <ul>
	 *    <li>log message String < 500 characters
	 *    <li>log formatter present
	 * </ul>
	 * 
	 * @throws NullPointerException  recIn is null
	 * 
	 * @see StreamHandler
	 */
	
	@Override
	public boolean isLoggable(final LogRecord recIn)
	{
		checkNotNull(recIn, "Null detected: LogRecord upon isLoggable call");
		
		final Optional<LogRecord> recInPossibly = Optional.fromNullable(recIn);
		
		if(!recInPossibly.isPresent()) return false;
		
		if(!_formatter.isPresent())
		{ 
			System.err.println("Unable to obtain log formatter");
			return false;
			
		}//if
		
		final String logMsg = recIn.getMessage();
		
		
		final Range<Integer> logMsgLen = Range.openClosed(0,7000);
		
		if( !logMsgLen.contains(logMsg.length()) ){
		
			Logger.getGlobal().warning("Reject log message due to length" + 
					logMsg.substring(1, 500) + "...");
			
			return false;
		
		}//if
		

		return true;
		
	}//method
	
	
	/* Predicate methods */
	
	/**
	 * Checks if this instance initialized successfully.
	 * 
	 * @return boolean  T constructor finished, log file created, log Path OK
	 */
	
	public boolean isReady()
	{
		return getLogFilePath().isPresent();
		
	}//method
	
	
	/* Security methods */
	
	
	/**
	 * Prevents serialization.
	 * 
	 * @param out  ObjectOutputStream, null OK
	 * 
	 * @throws IOException  attempted serialization
	 */
	
	private final void writeObject(ObjectOutputStream out) throws IOException 
	{
		throw new IOException("Serialization forbidden");
		
	}//method
	
	
	/**
	 * Prevents de-serialization.
	 * 
	 * @param in  ObjectInputStream, null OK
	 * 
	 * @throws IOException  attempted de-serialization
	 */
	
	private final void readObject(ObjectInputStream in) throws IOException 
	{
		throw new IOException("Deserialization forbidden");
		
	}//method	
	
	
	/**
	 * Prevents clone
	 * 
	 * @throws CloneNotSupportedException  attempt to clone
	 */
	
	public final LogCustom clone() throws CloneNotSupportedException
	{
		throw new CloneNotSupportedException("Cloning forbidden");
		
	}//method
	
	
	/* encap methods */
	
	
	/**
	 * Sets Path object for this CustomLog.
	 * 
	 * @param pathIn  Path of logfile for this CustomLog, not null
	 * 
	 * @throws NullPointerException  pathIn is null
	 */
	
	private void setLogFilePath(final Path pathIn) throws NullPointerException
	{
		checkNotNull(pathIn, "Null detected: Path pathIn parameter");

		_logFilePath = Optional.of(pathIn);
		
	}//method

	
	/**
	 * Gets Optional containing Path object for this CustomLog instance.
	 * 
	 * @return 
	 */
	
	private Optional<Path> getLogFilePath()
	{
		return _logFilePath;
		
	}//method
	
	
}//class


/**
 * Log Formatter<p> 
 *
 * Text log format including: thread id, date, log level, message.
 */

final class CustomLogFormatter extends Formatter 
{
	/**
	 * Custom Log Formatter<p>
	 * 
	 * Text log format including: thread id, date, log level, message.
	 */
	
	public CustomLogFormatter() 
	{
		super();
		
	}//constructor


	/**
	 * Text log format including: thread id, date, log level, message.
	 * 
	 * @param record  LogRecord to be formatted as String
	 * 
	 * @return String  formatted String
	 */
	
	@Override
	public String format(LogRecord record) 
	{	
		final StringBuffer sb = new StringBuffer();
		
		sb.append(record.getThreadID());
		sb.append("_|_");
		
		sb.append(record.getMillis());
		sb.append("_|_");
		
		sb.append(record.getLevel());
		sb.append("_|_");

		sb.append(super.formatMessage(record));
		sb.append("\n");

		return sb.toString();
	
	}//method

	
	/**
	 * Override of Formatter.<p>
	 * 
	 * Do not use, otherwise logger defaults to SimpleFormatter.
	 * 
	 * Deprecated as warning against calling directly from this subclass.<p>
	 * 
	 * Note required super.formatMessage usage else StackOverflow due to this 
	 * Override as causes looping/recursion.
	 * 
	 * @see format(LogRecord)
	 */

	@Deprecated
	@Override
	public String formatMessage(LogRecord record) 
	{
		return super.formatMessage(record);
		
	}//method
	
	
	/* Security methods */
	
	
	/**
	 * Prevent serialization
	 * 
	 * @param out  ObjectOutputStream representing instance of this class
	 * 
	 * @throws IOException  attempted serialization
	 */
	
	private final void writeObject(ObjectOutputStream out) throws IOException 
	{
		throw new IOException("Serialization forbidden");
		
	}//method
	
	
	/**
	 * Prevent de-serialization
	 * 
	 * @param in  ObjectInputStream representing instance of this class
	 * 
	 * @throws IOException  attempted de-serialization
	 */
	
	private final void readObject(ObjectInputStream in) throws IOException 
	{
		throw new IOException("Deserialization forbidden");
		
	}//method	
	
	
	/**
	 * Prevent clone
	 * 
	 * @throws CloneNotSupportedException  attempt to clone
	 */
	
	public final CustomLogFormatter clone() throws CloneNotSupportedException
	{
		throw new CloneNotSupportedException("Cloning forbidden");
		
	}//method
		
	
}//class


/**
 * CustomLogger.<p>
 * 
 * Supplies entering, exiting methods using custom logger.<p>
 * 
 * <i>Do not use for <u>custom</u> logger creation. Instead:</i><ul> 
 * 
 * <li><code>LogCustom.obtainLog(String logName)</code>
 * <li><code>LogCustom.obtainLog(String logName, Level logLvl)</code></ul>
 */

final class CustomLogger extends Logger
{
	public CustomLogger(final String loggerName, final String resourceBundle)
	{
		super(loggerName, resourceBundle);
		
	}//constructor
	

	public CustomLogger(final String loggerName)
	{
		super(loggerName, null);
		
	}//constructor

	
	@Override
	public void entering(final String classNameIn, final String methodNameIn)
	{
		super.finer("Start : " + classNameIn + " : " + methodNameIn);
		
	}//method

		
	@Override
	public void exiting(final String classNameIn, final String methodNameIn)
	{
		super.finer("Fini : " + classNameIn + " : " + methodNameIn);
		
	}//method

	
	/**
	 * Creates or retrieves default Logger.<p>
	 * 
	 * <i>Do not use for <u>custom</u> logger creation. Instead:</i><ul> 
	 * 
	 * <li><code>LogCustom.obtainLog(String logName)</code>
	 * <li><code>LogCustom.obtainLog(String logName, Level logLvl)</code></ul>
	 */
	
	public static CustomLogger getLogger(final String logName)
	{
		final LogManager logMgr = LogManager.getLogManager();
		
		final Optional<Logger> loggerWanted = Optional.fromNullable(logMgr
				.getLogger(logName));
		
		if( loggerWanted.isPresent() )
		{ 
			return (CustomLogger) loggerWanted.get();
			
		}//if
		
		logMgr.addLogger(new CustomLogger(logName, null));

		return (CustomLogger) logMgr.getLogger(logName);
			
	}//method
	
}//class